/*
 * Camera.java 16 juin 07
 *
 * Sweet Home 3D, Copyright (c) 2007 Emmanuel PUYBARET / eTeks <info@eteks.com>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */
package com.eteks.sweethome3d.model;

import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.TimeZone;

/**
 * Camera characteristics in home.
 * @author Emmanuel Puybaret
 */
public class Camera implements Serializable, Cloneable
{
	/**
	 * The kind of lens that can be used with a camera.
	 * @author Emmanuel Puybaret
	 * @since 3.0
	 */
	public enum Lens
	{
		PINHOLE, NORMAL, FISHEYE, SPHERICAL
	}
	
	/**
	 * The properties of a camera that may change. <code>PropertyChangeListener</code>s added 
	 * to a camera will be notified under a property name equal to the string value of one these properties.
	 */
	public enum Property
	{
		NAME, X, Y, Z, YAW, PITCH, FIELD_OF_VIEW, TIME, LENS
	}
	
	private static final long serialVersionUID = 1L;
	
	private String name;
	private float x;
	private float y;
	private float z;
	private float yaw;
	private float pitch;
	private float fieldOfView;
	private long time;
	private transient Lens lens;
	// Lens is saved as a string to be able to keep backward compatibility 
	// if new constants are added to Lens enum in future versions
	private String lensName;
	
	private transient PropertyChangeSupport propertyChangeSupport = new PropertyChangeSupport(this);
	
	/**
	 * Creates a camera at given location and angles at midday and using a pinhole lens.
	 */
	public Camera(float x, float y, float z, float yaw, float pitch, float fieldOfView)
	{
		this(x, y, z, yaw, pitch, fieldOfView, midday(), Lens.PINHOLE);
	}
	
	/**
	 * Creates a camera at given location and angles.
	 * @since 3.0
	 */
	public Camera(float x, float y, float z, float yaw, float pitch, float fieldOfView, long time, Lens lens)
	{
		this.x = x;
		this.y = y;
		this.z = z;
		this.yaw = yaw;
		this.pitch = pitch;
		this.fieldOfView = fieldOfView;
		this.time = time;
		this.lens = lens;
	}
	
	/**
	 * Returns the time of midday today in milliseconds since the Epoch in UTC time zone.
	 */
	private static long midday()
	{
		Calendar midday = new GregorianCalendar(TimeZone.getTimeZone("UTC"));
		midday.set(Calendar.HOUR_OF_DAY, 12);
		midday.set(Calendar.MINUTE, 0);
		midday.set(Calendar.SECOND, 0);
		return midday.getTimeInMillis();
	}
	
	/**
	 * Initializes new camera transient fields  
	 * and reads its properties from <code>in</code> stream with default reading method.
	 */
	private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException
	{
		this.propertyChangeSupport = new PropertyChangeSupport(this);
		this.time = midday();
		this.lens = Lens.PINHOLE;
		in.defaultReadObject();
		try
		{
			// Read lens from a string 
			if (this.lensName != null)
			{
				this.lens = Lens.valueOf(this.lensName);
			}
		}
		catch (IllegalArgumentException ex)
		{
			// Ignore malformed enum constant 
		}
	}
	
	private void writeObject(java.io.ObjectOutputStream out) throws IOException
	{
		// Write lens as a string to be able to read lens later 
		// even if enum changed in later versions
		this.lensName = this.lens.name();
		out.defaultWriteObject();
	}
	
	/**
	 * Adds the property change <code>listener</code> in parameter to this camera.
	 */
	public void addPropertyChangeListener(PropertyChangeListener listener)
	{
		this.propertyChangeSupport.addPropertyChangeListener(listener);
	}
	
	/**
	 * Removes the property change <code>listener</code> in parameter from this camera.
	 */
	public void removePropertyChangeListener(PropertyChangeListener listener)
	{
		this.propertyChangeSupport.removePropertyChangeListener(listener);
	}
	
	/**
	 * Returns the name of this camera.
	 * @since 3.0
	 */
	public String getName()
	{
		return this.name;
	}
	
	/**
	 * Sets the name of this camera and notifies listeners of this change. 
	 * @since 3.0
	 */
	public void setName(String name)
	{
		if (name != this.name && (name == null || !name.equals(this.name)))
		{
			String oldName = this.name;
			this.name = name;
			this.propertyChangeSupport.firePropertyChange(Property.NAME.name(), oldName, name);
		}
	}
	
	/**
	 * Returns the yaw angle in radians of this camera.
	 */
	public float getYaw()
	{
		return this.yaw;
	}
	
	/**
	 * Sets the yaw angle in radians of this camera.
	 */
	public void setYaw(float yaw)
	{
		if (yaw != this.yaw)
		{
			float oldYaw = this.yaw;
			this.yaw = yaw;
			this.propertyChangeSupport.firePropertyChange(Property.YAW.name(), oldYaw, yaw);
		}
	}
	
	/**
	 * Returns the pitch angle in radians of this camera.
	 */
	public float getPitch()
	{
		return this.pitch;
	}
	
	/**
	 * Sets the pitch angle in radians of this camera.
	 */
	public void setPitch(float pitch)
	{
		if (pitch != this.pitch)
		{
			float oldPitch = this.pitch;
			this.pitch = pitch;
			this.propertyChangeSupport.firePropertyChange(Property.PITCH.name(), oldPitch, pitch);
		}
	}
	
	/**
	 * Returns the field of view in radians of this camera.
	 */
	public float getFieldOfView()
	{
		return this.fieldOfView;
	}
	
	/**
	 * Sets the field of view in radians of this camera.
	 */
	public void setFieldOfView(float fieldOfView)
	{
		if (fieldOfView != this.fieldOfView)
		{
			float oldFieldOfView = this.fieldOfView;
			this.fieldOfView = fieldOfView;
			this.propertyChangeSupport.firePropertyChange(Property.FIELD_OF_VIEW.name(), oldFieldOfView, fieldOfView);
		}
	}
	
	/**
	 * Returns the abscissa of this camera.
	 */
	public float getX()
	{
		return this.x;
	}
	
	/**
	 * Sets the abscissa of this camera.
	 */
	public void setX(float x)
	{
		if (x != this.x)
		{
			float oldX = this.x;
			this.x = x;
			this.propertyChangeSupport.firePropertyChange(Property.X.name(), oldX, x);
		}
	}
	
	/**
	 * Returns the ordinate of this camera.
	 */
	public float getY()
	{
		return this.y;
	}
	
	/**
	 * Sets the ordinate of this camera.
	 */
	public void setY(float y)
	{
		if (y != this.y)
		{
			float oldY = this.y;
			this.y = y;
			this.propertyChangeSupport.firePropertyChange(Property.Y.name(), oldY, y);
		}
	}
	
	/**
	 * Returns the elevation of this camera.
	 */
	public float getZ()
	{
		return this.z;
	}
	
	/**
	 * Sets the elevation of this camera.
	 */
	public void setZ(float z)
	{
		if (z != this.z)
		{
			float oldZ = this.z;
			this.z = z;
			this.propertyChangeSupport.firePropertyChange(Property.Z.name(), oldZ, z);
		}
	}
	
	/**
	 * Returns the time in milliseconds when this camera is used.
	 * @return a time in milliseconds since the Epoch in UTC time zone
	 * @since 3.0
	 */
	public long getTime()
	{
		return this.time;
	}
	
	/**
	 * Sets the use time in milliseconds since the Epoch in UTC time zone, 
	 * and notifies listeners of this change. 
	 * @since 3.0
	 */
	public void setTime(long time)
	{
		if (this.time != time)
		{
			long oldTime = this.time;
			this.time = time;
			this.propertyChangeSupport.firePropertyChange(Property.TIME.name(), oldTime, time);
		}
	}
	
	/**
	 * Returns a time expressed in UTC time zone converted to the given time zone. 
	 * @since 3.0
	 */
	public static long convertTimeToTimeZone(long utcTime, String timeZone)
	{
		Calendar utcCalendar = new GregorianCalendar(TimeZone.getTimeZone("UTC"));
		utcCalendar.setTimeInMillis(utcTime);
		Calendar convertedCalendar = new GregorianCalendar(TimeZone.getTimeZone(timeZone));
		convertedCalendar.set(Calendar.YEAR, utcCalendar.get(Calendar.YEAR));
		convertedCalendar.set(Calendar.MONTH, utcCalendar.get(Calendar.MONTH));
		convertedCalendar.set(Calendar.DAY_OF_MONTH, utcCalendar.get(Calendar.DAY_OF_MONTH));
		convertedCalendar.set(Calendar.HOUR_OF_DAY, utcCalendar.get(Calendar.HOUR_OF_DAY));
		convertedCalendar.set(Calendar.MINUTE, utcCalendar.get(Calendar.MINUTE));
		convertedCalendar.set(Calendar.SECOND, utcCalendar.get(Calendar.SECOND));
		convertedCalendar.set(Calendar.MILLISECOND, utcCalendar.get(Calendar.MILLISECOND));
		return convertedCalendar.getTimeInMillis();
	}
	
	/**
	 * Returns the lens of this camera.
	 * @since 3.0
	 */
	public Lens getLens()
	{
		return this.lens;
	}
	
	/**
	 * Sets the lens of this camera.
	 * @since 3.0
	 */
	public void setLens(Lens lens)
	{
		if (lens != this.lens)
		{
			Lens oldLens = this.lens;
			this.lens = lens;
			this.propertyChangeSupport.firePropertyChange(Property.LENS.name(), oldLens, lens);
		}
	}
	
	/**
	 * Sets the location and angles of this camera from the <code>camera</code> in parameter.
	 * @since 2.3
	 */
	public void setCamera(Camera camera)
	{
		setX(camera.getX());
		setY(camera.getY());
		setZ(camera.getZ());
		setYaw(camera.getYaw());
		setPitch(camera.getPitch());
		setFieldOfView(camera.getFieldOfView());
	}
	
	/**
	 * Returns a clone of this camera.
	 * @since 2.3
	 */
	@Override
	public Camera clone()
	{
		try
		{
			return (Camera) super.clone();
		}
		catch (CloneNotSupportedException ex)
		{
			throw new IllegalStateException("Super class isn't cloneable");
		}
	}
}
